#pragma once

#include"Socket.hpp"
#include"InetAddr.hpp"
#include<iostream>

static const int gsize = sizeof(fd_set)*8;//数组大小
static const int gdefault = -1;//初始化数组的值

//select 一次可以等待多个文件文件描述符，当一个文件准备好了就会通知用户；就绪事件通知机制
//select 的第一个参数为最大文件描述符值+1 ，第2、3、4 为读、写、异常的位图，需要select 关心哪些文件描述符就添加进位图当中;且均为输入输出型参数
//struct timeval* timeout , 设置select 的等待时间，也为输入输出型参数；设置为NULL代表阻塞等待
//struct timeval 中有两个成员: 秒和微秒；设置为{0,0} 表示非阻塞等待；
// 设置为{5,0}表示会将所有fd 军检测一遍，发现没有就绪的，select 就会等待5秒，在这5秒内，如果有文件描述符准备好了，select 就返回，如果没有则timeout
//以非阻塞的方式返回；输入输出型参数，输入时表示等待多长的时间，输出时表示剩余多长的时间；
//fd_set 位图，输入时告诉内核需要关心哪些文件描述符；输出时内核会告诉用户哪些文件描述符就绪了；

class SelectServer//基于tcp
{
public:
    SelectServer(uint16_t port)
    :_listensockfd(std::make_unique<TcpSocket>())
    {
        //创建监听套接字
        _listensockfd->BuildListenSocket(port);
        //在构造的时候初始化保存文件描述符的数组
        for(int i = 0;i < gsize;i++)
        {
            fd_array[i] = gdefault;
        }
        //规定该数组中保存的第一个文件描述符为监听套接字
        fd_array[0] = _listensockfd->Sockfd();
    }

    void Accepter()
    {
        //获取连接
        InetAddr client;
        int sockfd = _listensockfd->Accept(&client);
        //判断sockfd 
        if(sockfd > 0)
        {
            LOG(LogLevel::INFO) << "获取到新连接: " << sockfd << ",  client addr: " << client.ToString();
        }
        //将获取到的新文件描述符也交给select 来管理，新增一个成员属性 --> 数组,select 能管理的文件描述符是有范围的，sizeof(fd_set)*8
        //在fd-array 中查找空位置
        int pos = 0;
        for( ; pos < gsize;pos++)
        {
            if(fd_array[pos]==gdefault) 
            {
                fd_array[pos] = sockfd;
                break;
            }
        }
        //如果没有找到，就达到了服务器接受的上限
        if(pos >= gsize)
        {
            LOG(LogLevel::WARNING) << "服务器满载";
            //直接关闭这个链接
            close(sockfd);
        }
    }

    void Recver(int index)
    {
        //读取数据，此处还有缺陷，后面再封装协议
        int sockfd = fd_array[index];
        char buffer[1024];  
        int n = recv(sockfd , buffer , sizeof(buffer)-1 , 0);
        if(n==0)//等于0意味着对端退出
        {
            //那么服务器端也退出，关闭此文件描述符，且让select 不再关心该文件描述符
            LOG(LogLevel::INFO) << "客户端退出， 我也退出: " << sockfd;
            close(sockfd);
            fd_array[index] = gdefault;
        }
        else if(n>0)
        {
            //大于0表示读取成功，返回成功读取的字节数
            buffer[n]=0;
            //debug
            std::cout << "client say# " << buffer << std::endl;
            //构建应答，使用send 直接发送回去；之所以不交付给select 是因为，大多数情况下输出都是满足的
            std::string send_str = "server say#";
            send_str += buffer;
            send(sockfd , send_str.c_str() , send_str.size(),0);
        }
        else
        {
            //小于0就是读取错误
            LOG(LogLevel::WARNING) << "读取错误,  sockfd: " << fd_array[index];
            //同样需要关闭文件描述符，以及让select 不要再关心这个文件描述符的读
            close(sockfd);
            fd_array[index] = gdefault;
        }
    }

    //因为select 会随着连接增多，而要检测的文件描述符越来越多，所以HeadlerEvents 不能简单地处理监听套接字；因为就绪的文件不一定是监听套接字
    //而有可能为其他文件描述符，此处需要将rfds 中传递进HeadlerEvents 中，遍历 fd_array 中的合法文件描述符利用FD_ISSET来判断哪些文件描述符
    //就绪了；为监听套接字就获取链接，而普通文件描述符，由于此处我们只关心读，所以就去读数据就好了 --> 将获取连接的模块分离出来
    //另外select 的核心在于IO中等待多个fd（通过位图），任意一个或多个文件描述符就绪就会通知用户；
    // 所以select 的返回值大于0的时候就一定会有就绪事件，可以使获取连接也可以是处理读；那么HeadlerEvents 就不太合理，应该叫做
    // 事件派发器，当有时间就绪了，就将事件派发到不同的模块中，即EventDispatcher
    void EventDispacher(fd_set& rfds)
    {
        //处理就绪事件
        LOG(LogLevel::DEBUG) << "fd就绪，有新事件到来";
        //遍历fd_array,判断哪些文件描述符就绪了
        for(int i = 0; i<gsize;i++)
        {
            if(fd_array[i]==gdefault) continue;

            //合法的文件描述符，利用FD_ISSET来判断
            if(FD_ISSET(fd_array[i] , &rfds))
            {
                //判断是监听套接字还是普通文件描述符
                if(fd_array[i] == _listensockfd->Sockfd())
                {
                    Accepter();//获取连接模块
                }
                else
                {
                    //普通文件
                    Recver(i);
                }
            }
        }
    }

    void Run()
    {
        //不能立即接受链接，而是交给select 来处理
        while(true)
        {
            // int select(int nfds, fd_set *_Nullable restrict readfds,
            // fd_set *_Nullable restrict writefds,
            // fd_set *_Nullable restrict exceptfds,
            // struct timeval *_Nullable restrict timeout);
            //seletct 的返回值，大于0，表示已经就绪的fd 个数，等于0表示没有就绪的文件，小于0表示select 调用出错
            // int maxfd = _listensockfd->Sockfd();
            int maxfd = -1;//最大文件描述符值应该随着文件描述符的增多而变化
            struct timeval timeout = {3,0};
            fd_set rfds;
            FD_ZERO(&rfds);//置空
            
            //便利fd_array ，将其中的文件描述符添加到所需要关心的位图中
            for(int i = 0;i<gsize ; i++)
            {
                if(fd_array[i] == gdefault) continue;
                FD_SET( fd_array[i], &rfds);//将所关心的文件描述符设置进位图

                //并且求到最大文件描述符
                 if(maxfd < fd_array[i])
                 {
                    maxfd = fd_array[i];
                 }
                 //debug
                 LOG(LogLevel::DEBUG) << "添加fd: " << fd_array[i];                
            }

            int n = select(maxfd+1, &rfds , nullptr , nullptr , &timeout);
            //判断select 的返回值
            switch (n)
            {
                case 0:
                    LOG(LogLevel::DEBUG) << "timeout……" << timeout.tv_sec << ": " << timeout.tv_usec;
                    break;
                case -1:
                    LOG(LogLevel::DEBUG) << "select error";
                    break;
                default://有文件描述符就绪
                    EventDispacher(rfds);
                    break;
            }
        }
    }
    ~SelectServer(){}

private:
    //监听套接字
    std::unique_ptr<TcpSocket> _listensockfd;
    int fd_array[gsize];
};